/*
 * HSCarousel
 * @version: 3.2.3
 * @author: Preline Labs Ltd.
 * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
 * Copyright 2024 Preline Labs Ltd.
 */

import { classToClassList, debounce, htmlToElement } from '../../utils'

import { ICarousel, ICarouselOptions } from './interfaces'
import { TCarouselOptionsSlidesQty } from './types'

import HSBasePlugin from '../base-plugin'
import { ICollectionItem } from '../../interfaces'
import { BREAKPOINTS } from '../../constants'

class HSCarousel extends HSBasePlugin<ICarouselOptions> implements ICarousel {
  private currentIndex: number
  private readonly loadingClasses: string | string[]
  private readonly dotsItemClasses: string
  private readonly isAutoHeight: boolean
  private readonly isAutoPlay: boolean
  private readonly isCentered: boolean
  private readonly isDraggable: boolean
  private readonly isInfiniteLoop: boolean
  private readonly isRTL: boolean
  private readonly isSnap: boolean
  private readonly hasSnapSpacers: boolean
  private readonly slidesQty: TCarouselOptionsSlidesQty | number
  private readonly speed: number
  private readonly updateDelay: number

  private readonly loadingClassesRemove: string | string[]
  private readonly loadingClassesAdd: string | string[]
  private readonly afterLoadingClassesAdd: string | string[]

  private readonly container: HTMLElement | null
  private readonly inner: HTMLElement | null
  private readonly slides: NodeListOf<HTMLElement> | undefined[]
  private readonly prev: HTMLElement | null
  private readonly next: HTMLElement | null
  private readonly dots: HTMLElement | null
  private dotsItems: NodeListOf<HTMLElement> | undefined[] | null
  private readonly info: HTMLElement | null
  private readonly infoTotal: HTMLElement | null
  private readonly infoCurrent: HTMLElement | null

  private sliderWidth: number
  private timer: any

  // Drag events' help variables
  private isScrolling: ReturnType<typeof setTimeout>
  private isDragging: boolean
  private dragStartX: number | null
  private initialTranslateX: number | null

  // Touch events' help variables
  private readonly touchX: {
    start: number
    end: number
  }
  private readonly touchY: {
    start: number
    end: number
  }

  // Resize events' help variables
  private resizeContainer: HTMLElement
  public resizeContainerWidth: number

  // Listeners
  private onPrevClickListener: () => void
  private onNextClickListener: () => void
  private onContainerScrollListener: () => void
  private onElementTouchStartListener: (evt: TouchEvent) => void
  private onElementTouchEndListener: (evt: TouchEvent) => void
  private onInnerMouseDownListener: (evt: MouseEvent | TouchEvent) => void
  private onInnerTouchStartListener: (evt: MouseEvent | TouchEvent) => void
  private onDocumentMouseMoveListener: (evt: MouseEvent | TouchEvent) => void
  private onDocumentTouchMoveListener: (evt: MouseEvent | TouchEvent) => void
  private onDocumentMouseUpListener: () => void
  private onDocumentTouchEndListener: () => void
  private onDotClickListener: () => void

  constructor(el: HTMLElement, options?: ICarouselOptions) {
    super(el, options)

    const data = el.getAttribute('data-carousel')
    const dataOptions: ICarouselOptions = data ? JSON.parse(data) : {}
    const concatOptions = {
      ...dataOptions,
      ...options
    }

    this.currentIndex = concatOptions.currentIndex || 0
    this.loadingClasses = concatOptions.loadingClasses ? `${concatOptions.loadingClasses}`.split(',') : null
    this.dotsItemClasses = concatOptions.dotsItemClasses ? concatOptions.dotsItemClasses : null
    this.isAutoHeight = typeof concatOptions.isAutoHeight !== 'undefined' ? concatOptions.isAutoHeight : false
    this.isAutoPlay = typeof concatOptions.isAutoPlay !== 'undefined' ? concatOptions.isAutoPlay : false
    this.isCentered = typeof concatOptions.isCentered !== 'undefined' ? concatOptions.isCentered : false
    this.isDraggable = typeof concatOptions.isDraggable !== 'undefined' ? concatOptions.isDraggable : false
    this.isInfiniteLoop = typeof concatOptions.isInfiniteLoop !== 'undefined' ? concatOptions.isInfiniteLoop : false
    this.isRTL = typeof concatOptions.isRTL !== 'undefined' ? concatOptions.isRTL : false
    this.isSnap = typeof concatOptions.isSnap !== 'undefined' ? concatOptions.isSnap : false
    this.hasSnapSpacers = typeof concatOptions.hasSnapSpacers !== 'undefined' ? concatOptions.hasSnapSpacers : true
    this.speed = concatOptions.speed || 4000
    this.updateDelay = concatOptions.updateDelay || 0
    this.slidesQty = concatOptions.slidesQty || 1

    this.loadingClassesRemove = this.loadingClasses?.[0] ? this.loadingClasses[0].split(' ') : 'opacity-0'
    this.loadingClassesAdd = this.loadingClasses?.[1] ? this.loadingClasses[1].split(' ') : ''
    this.afterLoadingClassesAdd = this.loadingClasses?.[2] ? this.loadingClasses[2].split(' ') : ''

    this.container = this.el.querySelector('.carousel') || null
    this.inner = this.el.querySelector('.carousel-body') || null
    this.slides = this.el.querySelectorAll('.carousel-slide') || []
    this.prev = this.el.querySelector('.carousel-prev') || null
    this.next = this.el.querySelector('.carousel-next') || null
    this.dots = this.el.querySelector('.carousel-pagination') || null
    this.info = this.el.querySelector('.carousel-info') || null
    this.infoTotal = this?.info?.querySelector('.carousel-info-total') || null
    this.infoCurrent = this?.info?.querySelector('.carousel-info-current') || null

    this.sliderWidth = this.el.getBoundingClientRect().width

    // Drag events' help variables
    this.isDragging = false
    this.dragStartX = null
    this.initialTranslateX = null

    // Touch events' help variables
    this.touchX = {
      start: 0,
      end: 0
    }
    this.touchY = {
      start: 0,
      end: 0
    }

    // Resize events' help variables
    this.resizeContainer = document.querySelector('body')
    this.resizeContainerWidth = 0

    this.init()
  }

  private setIsSnap() {
    const containerRect = this.container.getBoundingClientRect()
    const containerCenter = containerRect.left + containerRect.width / 2

    let closestElement: HTMLElement | null = null
    let closestElementIndex: number | null = null
    let closestDistance = Infinity

    Array.from(this.inner.children).forEach((child: HTMLElement) => {
      const childRect = child.getBoundingClientRect()
      const innerContainerRect = this.inner.getBoundingClientRect()
      const childCenter = childRect.left + childRect.width / 2 - innerContainerRect.left
      const distance = Math.abs(containerCenter - (innerContainerRect.left + childCenter))

      if (distance < closestDistance) {
        closestDistance = distance
        closestElement = child
      }
    })

    if (closestElement) {
      closestElementIndex = Array.from(this.slides).findIndex(el => el === closestElement)
    }

    this.setIndex(closestElementIndex)

    if (this.dots) this.setCurrentDot()
  }

  private prevClick() {
    this.goToPrev()
    if (this.isAutoPlay) {
      this.resetTimer()
      this.setTimer()
    }
  }

  private nextClick() {
    this.goToNext()
    if (this.isAutoPlay) {
      this.resetTimer()
      this.setTimer()
    }
  }

  private containerScroll() {
    clearTimeout(this.isScrolling)

    this.isScrolling = setTimeout(() => {
      this.setIsSnap()
    }, 100)
  }

  private elementTouchStart(evt: TouchEvent) {
    this.touchX.start = evt.changedTouches[0].screenX
    this.touchY.start = evt.changedTouches[0].screenY
  }

  private elementTouchEnd(evt: TouchEvent) {
    this.touchX.end = evt.changedTouches[0].screenX
    this.touchY.end = evt.changedTouches[0].screenY

    this.detectDirection()
  }

  private innerMouseDown(evt: MouseEvent | TouchEvent) {
    this.handleDragStart(evt)
  }

  private innerTouchStart(evt: MouseEvent | TouchEvent) {
    this.handleDragStart(evt)
  }

  private documentMouseMove(evt: MouseEvent | TouchEvent) {
    this.handleDragMove(evt)
  }

  private documentTouchMove(evt: MouseEvent | TouchEvent) {
    this.handleDragMove(evt)
  }

  private documentMouseUp() {
    this.handleDragEnd()
  }

  private documentTouchEnd() {
    this.handleDragEnd()
  }

  private dotClick(ind: number) {
    this.goTo(ind)

    if (this.isAutoPlay) {
      this.resetTimer()
      this.setTimer()
    }
  }

  private init() {
    this.createCollection(window.$hsCarouselCollection, this)

    if (this.inner) {
      this.calculateWidth()

      if (this.isDraggable && !this.isSnap) this.initDragHandling()
    }

    if (this.prev) {
      this.onPrevClickListener = () => this.prevClick()

      this.prev.addEventListener('click', this.onPrevClickListener)
    }

    if (this.next) {
      this.onNextClickListener = () => this.nextClick()

      this.next.addEventListener('click', this.onNextClickListener)
    }

    if (this.dots) this.initDots()
    if (this.info) this.buildInfo()
    if (this.slides.length) {
      this.addCurrentClass()
      if (!this.isInfiniteLoop) this.addDisabledClass()
      if (this.isAutoPlay) this.autoPlay()
    }

    setTimeout(() => {
      if (this.isSnap) this.setIsSnap()

      if (this.loadingClassesRemove) {
        if (typeof this.loadingClassesRemove === 'string') {
          this.inner.classList.remove(this.loadingClassesRemove)
        } else this.inner.classList.remove(...this.loadingClassesRemove)
      }
      if (this.loadingClassesAdd) {
        if (typeof this.loadingClassesAdd === 'string') {
          this.inner.classList.add(this.loadingClassesAdd)
        } else this.inner.classList.add(...this.loadingClassesAdd)
      }

      if (this.inner && this.afterLoadingClassesAdd) {
        setTimeout(() => {
          if (typeof this.afterLoadingClassesAdd === 'string') {
            this.inner.classList.add(this.afterLoadingClassesAdd)
          } else this.inner.classList.add(...this.afterLoadingClassesAdd)
        })
      }
    }, 400)

    if (this.isSnap) {
      this.onContainerScrollListener = () => this.containerScroll()

      this.container.addEventListener('scroll', this.onContainerScrollListener)
    }

    this.el.classList.add('init')

    if (!this.isSnap) {
      this.onElementTouchStartListener = (evt: TouchEvent) => this.elementTouchStart(evt)
      this.onElementTouchEndListener = (evt: TouchEvent) => this.elementTouchEnd(evt)

      this.el.addEventListener('touchstart', this.onElementTouchStartListener)

      this.el.addEventListener('touchend', this.onElementTouchEndListener)
    }

    this.observeResize()
  }

  private initDragHandling(): void {
    const scrollableElement = this.inner

    this.onInnerMouseDownListener = evt => this.innerMouseDown(evt)
    this.onInnerTouchStartListener = evt => this.innerTouchStart(evt)
    this.onDocumentMouseMoveListener = evt => this.documentMouseMove(evt)
    this.onDocumentTouchMoveListener = evt => this.documentTouchMove(evt)
    this.onDocumentMouseUpListener = () => this.documentMouseUp()
    this.onDocumentTouchEndListener = () => this.documentTouchEnd()

    if (scrollableElement) {
      scrollableElement.addEventListener('mousedown', this.onInnerMouseDownListener)
      scrollableElement.addEventListener('touchstart', this.onInnerTouchStartListener, { passive: true })

      document.addEventListener('mousemove', this.onDocumentMouseMoveListener)
      document.addEventListener('touchmove', this.onDocumentTouchMoveListener, {
        passive: false
      })

      document.addEventListener('mouseup', this.onDocumentMouseUpListener)
      document.addEventListener('touchend', this.onDocumentTouchEndListener)
    }
  }

  private getTranslateXValue(): number {
    const transformMatrix = window.getComputedStyle(this.inner).transform

    if (transformMatrix !== 'none') {
      const matrixValues = transformMatrix.match(/matrix.*\((.+)\)/)?.[1].split(', ')

      if (matrixValues) {
        let translateX = parseFloat(matrixValues.length === 6 ? matrixValues[4] : matrixValues[12])
        if (this.isRTL) translateX = -translateX

        return isNaN(translateX) || translateX === 0 ? 0 : -translateX
      }
    }
    return 0
  }

  private removeClickEventWhileDragging(evt: MouseEvent) {
    evt.preventDefault()
  }

  private handleDragStart(evt: MouseEvent | TouchEvent): void {
    evt.preventDefault()

    this.isDragging = true
    this.dragStartX = this.getEventX(evt)
    this.initialTranslateX = this.isRTL ? this.getTranslateXValue() : -this.getTranslateXValue()

    this.inner.classList.add('dragging')
  }

  private handleDragMove(evt: MouseEvent | TouchEvent): void {
    if (!this.isDragging) return

    this.inner.querySelectorAll('a:not(.prevented-click)').forEach(el => {
      el.classList.add('prevented-click')
      el.addEventListener('click', this.removeClickEventWhileDragging)
    })

    const currentX = this.getEventX(evt)
    let deltaX = currentX - this.dragStartX
    if (this.isRTL) deltaX = -deltaX
    const newTranslateX = this.initialTranslateX + deltaX
    const newTranslateXFunc = () => {
      let calcWidth = (this.sliderWidth * this.slides.length) / this.getCurrentSlidesQty() - this.sliderWidth
      const containerWidth = this.sliderWidth
      const itemWidth = containerWidth / this.getCurrentSlidesQty()
      const centeredOffset = (containerWidth - itemWidth) / 2
      const limitStart = this.isCentered ? centeredOffset : 0
      if (this.isCentered) calcWidth = calcWidth + centeredOffset
      const limitEnd = -calcWidth

      if (this.isRTL) {
        if (newTranslateX < limitStart) return limitStart
        if (newTranslateX > calcWidth) return limitEnd
        else return -newTranslateX
      } else {
        if (newTranslateX > limitStart) return limitStart
        else if (newTranslateX < -calcWidth) return limitEnd
        else return newTranslateX
      }
    }

    this.setTranslate(newTranslateXFunc())
  }

  private handleDragEnd(): void {
    if (!this.isDragging) return
    this.isDragging = false

    const containerWidth = this.sliderWidth
    const itemWidth = containerWidth / this.getCurrentSlidesQty()
    const currentTranslateX = this.getTranslateXValue()
    let closestIndex = Math.round(currentTranslateX / itemWidth)
    if (this.isRTL) closestIndex = Math.round(currentTranslateX / itemWidth)

    this.inner.classList.remove('dragging')

    setTimeout(() => {
      this.calculateTransform(closestIndex)
      if (this.dots) this.setCurrentDot()

      this.dragStartX = null
      this.initialTranslateX = null

      this.inner.querySelectorAll('a.prevented-click').forEach(el => {
        el.classList.remove('prevented-click')
        el.removeEventListener('click', this.removeClickEventWhileDragging)
      })
    })
  }

  private getEventX(event: MouseEvent | TouchEvent): number {
    return event instanceof MouseEvent ? event.clientX : event.touches[0].clientX
  }

  private getCurrentSlidesQty(): number {
    if (typeof this.slidesQty === 'object') {
      const windowWidth = document.body.clientWidth
      let currentRes = 0

      Object.keys(this.slidesQty).forEach((key: string) => {
        if (
          windowWidth >=
          (typeof key + 1 === 'number' ? (this.slidesQty as TCarouselOptionsSlidesQty)[key] : BREAKPOINTS[key])
        ) {
          currentRes = (this.slidesQty as TCarouselOptionsSlidesQty)[key]
        }
      })

      return currentRes
    } else {
      return this.slidesQty as number
    }
  }

  private buildSnapSpacers() {
    const existingBefore = this.inner.querySelector('.snap-before')
    const existingAfter = this.inner.querySelector('.snap-after')
    if (existingBefore) existingBefore.remove()
    if (existingAfter) existingAfter.remove()

    const containerWidth = this.sliderWidth
    const itemWidth = containerWidth / this.getCurrentSlidesQty()
    const spacerWidth = containerWidth / 2 - itemWidth / 2

    const before = htmlToElement(`<div class="snap-before" style="height: 100%; width: ${spacerWidth}px"></div>`)
    const after = htmlToElement(`<div class="snap-after" style="height: 100%; width: ${spacerWidth}px"></div>`)

    this.inner.prepend(before)
    this.inner.appendChild(after)
  }

  private initDots() {
    if (this.el.querySelectorAll('.carousel-pagination-item').length) {
      this.setDots()
    } else this.buildDots()

    if (this.dots) this.setCurrentDot()
  }

  private buildDots() {
    this.dots.innerHTML = ''

    const slidesQty =
      !this.isCentered && this.slidesQty ? this.slides.length - (this.getCurrentSlidesQty() - 1) : this.slides.length

    for (let i = 0; i < slidesQty; i++) {
      const singleDot = this.buildSingleDot(i)

      this.dots.append(singleDot)
    }
  }

  private setDots() {
    this.dotsItems = this.dots.querySelectorAll('.carousel-pagination-item')

    this.dotsItems.forEach((dot, ind) => {
      const targetIndex = dot.getAttribute('data-carousel-pagination-item-target')

      this.singleDotEvents(dot, targetIndex ? +targetIndex : ind)
    })
  }

  private goToCurrentDot() {
    const container = this.dots
    const containerRect = container.getBoundingClientRect()
    const containerScrollLeft = container.scrollLeft
    const containerScrollTop = container.scrollTop
    const containerWidth = container.clientWidth
    const containerHeight = container.clientHeight

    const item = this.dotsItems[this.currentIndex]
    const itemRect = item.getBoundingClientRect()
    const itemLeft = itemRect.left - containerRect.left + containerScrollLeft
    const itemRight = itemLeft + item.clientWidth
    const itemTop = itemRect.top - containerRect.top + containerScrollTop
    const itemBottom = itemTop + item.clientHeight

    let scrollLeft = containerScrollLeft
    let scrollTop = containerScrollTop

    if (itemLeft < containerScrollLeft || itemRight > containerScrollLeft + containerWidth) {
      scrollLeft = itemRight - containerWidth
    }

    if (itemTop < containerScrollTop || itemBottom > containerScrollTop + containerHeight) {
      scrollTop = itemBottom - containerHeight
    }

    container.scrollTo({
      left: scrollLeft,
      top: scrollTop,
      behavior: 'smooth'
    })
  }

  private buildInfo() {
    if (this.infoTotal) this.setInfoTotal()
    if (this.infoCurrent) this.setInfoCurrent()
  }

  private setInfoTotal() {
    this.infoTotal.innerText = `${this.slides.length}`
  }

  private setInfoCurrent() {
    this.infoCurrent.innerText = `${this.currentIndex + 1}`
  }

  private buildSingleDot(ind: number) {
    const singleDot = htmlToElement('<span></span>')
    if (this.dotsItemClasses) classToClassList(this.dotsItemClasses, singleDot)

    this.singleDotEvents(singleDot, ind)

    return singleDot
  }

  private singleDotEvents(dot: HTMLElement, ind: number) {
    this.onDotClickListener = () => this.dotClick(ind)

    dot.addEventListener('click', this.onDotClickListener)
  }

  private observeResize() {
    const resizeObserver = new ResizeObserver(
      debounce((entries: ResizeObserverEntry[]) => {
        for (let entry of entries) {
          const newWidth = entry.contentRect.width

          if (newWidth !== this.resizeContainerWidth) {
            this.recalculateWidth()
            if (this.dots) this.initDots()
            this.addCurrentClass()

            this.resizeContainerWidth = newWidth
          }
        }
      }, this.updateDelay)
    )

    resizeObserver.observe(this.resizeContainer)
  }

  private calculateWidth() {
    if (!this.isSnap) {
      this.inner.style.width = `${(this.sliderWidth * this.slides.length) / this.getCurrentSlidesQty()}px`
    }

    this.slides.forEach(el => {
      el.style.width = `${this.sliderWidth / this.getCurrentSlidesQty()}px`
    })

    this.calculateTransform()
  }

  private addCurrentClass() {
    if (this.isSnap) {
      const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2)

      for (let i = 0; i < this.slides.length; i++) {
        const slide = this.slides[i]

        if (i <= this.currentIndex + itemsQty && i >= this.currentIndex - itemsQty) {
          slide.classList.add('active')
        } else slide.classList.remove('active')
      }
    } else {
      const maxIndex = this.isCentered
        ? this.currentIndex + this.getCurrentSlidesQty() + (this.getCurrentSlidesQty() - 1)
        : this.currentIndex + this.getCurrentSlidesQty()

      this.slides.forEach((el, i) => {
        if (i >= this.currentIndex && i < maxIndex) {
          el.classList.add('active')
        } else {
          el.classList.remove('active')
        }
      })
    }
  }

  private setCurrentDot() {
    const toggleDotActive = (el: HTMLElement | Element, i: number) => {
      let statement = false
      const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2)

      if (this.isSnap && !this.hasSnapSpacers) {
        statement =
          i === (this.getCurrentSlidesQty() % 2 === 0 ? this.currentIndex - itemsQty + 1 : this.currentIndex - itemsQty)
      } else statement = i === this.currentIndex

      if (statement) el.classList.add('active')
      else el.classList.remove('active')
    }

    if (this.dotsItems) {
      this.dotsItems.forEach((el, i) => toggleDotActive(el, i))
    } else {
      this.dots.querySelectorAll(':scope > *').forEach((el, i) => toggleDotActive(el, i))
    }
  }

  private setElementToDisabled(el: HTMLElement) {
    el.classList.add('disabled')
    if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
      el.setAttribute('disabled', 'disabled')
    }
  }

  private unsetElementToDisabled(el: HTMLElement) {
    el.classList.remove('disabled')
    if (el.tagName === 'BUTTON' || el.tagName === 'INPUT') {
      el.removeAttribute('disabled')
    }
  }

  private addDisabledClass() {
    if (!this.prev || !this.next) return false

    const gapValue = getComputedStyle(this.inner).getPropertyValue('gap')
    const itemsQty = Math.floor(this.getCurrentSlidesQty() / 2)
    let currentIndex = 0
    let maxIndex = 0
    let statementPrev = false
    let statementNext = false

    if (this.isSnap) {
      currentIndex = this.currentIndex
      maxIndex = this.hasSnapSpacers ? this.slides.length - 1 : this.slides.length - itemsQty - 1
      statementPrev = this.hasSnapSpacers
        ? currentIndex === 0
        : this.getCurrentSlidesQty() % 2 === 0
          ? currentIndex - itemsQty < 0
          : currentIndex - itemsQty === 0
      statementNext =
        currentIndex >= maxIndex &&
        this.container.scrollLeft + this.container.clientWidth + (parseFloat(gapValue) || 0) >=
          this.container.scrollWidth
    } else {
      currentIndex = this.currentIndex
      maxIndex = this.isCentered
        ? this.slides.length - this.getCurrentSlidesQty() + (this.getCurrentSlidesQty() - 1)
        : this.slides.length - this.getCurrentSlidesQty()
      statementPrev = currentIndex === 0
      statementNext = currentIndex >= maxIndex
    }

    if (statementPrev) {
      this.unsetElementToDisabled(this.next)
      this.setElementToDisabled(this.prev)
    } else if (statementNext) {
      this.unsetElementToDisabled(this.prev)
      this.setElementToDisabled(this.next)
    } else {
      this.unsetElementToDisabled(this.prev)
      this.unsetElementToDisabled(this.next)
    }
  }

  private autoPlay() {
    this.setTimer()
  }

  private setTimer() {
    this.timer = setInterval(() => {
      if (this.currentIndex === this.slides.length - 1) this.goTo(0)
      else this.goToNext()
    }, this.speed)
  }

  private resetTimer() {
    clearInterval(this.timer)
  }

  private detectDirection() {
    const deltaX = this.touchX.end - this.touchX.start
    const deltaY = this.touchY.end - this.touchY.start
    const absDeltaX = Math.abs(deltaX)
    const absDeltaY = Math.abs(deltaY)
    const SWIPE_THRESHOLD = 30

    if (absDeltaX < SWIPE_THRESHOLD || absDeltaX < absDeltaY) return

    const isSwipeToNext = this.isRTL ? deltaX > 0 : deltaX < 0

    if (!this.isInfiniteLoop) {
      if (isSwipeToNext && this.currentIndex < this.slides.length - this.getCurrentSlidesQty()) {
        this.goToNext()
      }
      if (!isSwipeToNext && this.currentIndex > 0) {
        this.goToPrev()
      }
    } else {
      if (isSwipeToNext) this.goToNext()
      else this.goToPrev()
    }
  }

  private calculateTransform(currentIdx?: number | undefined): void {
    if (currentIdx !== undefined) this.currentIndex = currentIdx

    const containerWidth = this.sliderWidth
    const itemWidth = containerWidth / this.getCurrentSlidesQty()
    let translateX = this.currentIndex * itemWidth

    if (this.isSnap && !this.isCentered) {
      if (this.container.scrollLeft < containerWidth && this.container.scrollLeft + itemWidth / 2 > containerWidth) {
        this.container.scrollLeft = this.container.scrollWidth
      }
    }

    if (this.isCentered && !this.isSnap) {
      const centeredOffset = (containerWidth - itemWidth) / 2

      if (this.currentIndex === 0) translateX = -centeredOffset
      else if (
        this.currentIndex >=
        this.slides.length - this.getCurrentSlidesQty() + (this.getCurrentSlidesQty() - 1)
      ) {
        const totalSlideWidth = this.slides.length * itemWidth

        translateX = totalSlideWidth - containerWidth + centeredOffset
      } else translateX = this.currentIndex * itemWidth - centeredOffset
    }

    if (!this.isSnap) this.setTransform(translateX)

    if (this.isAutoHeight) {
      this.inner.style.height = `${this.slides[this.currentIndex].clientHeight}px`
    }

    if (this.dotsItems) this.goToCurrentDot()

    this.addCurrentClass()
    if (!this.isInfiniteLoop) this.addDisabledClass()
    if (this.isSnap && this.hasSnapSpacers) this.buildSnapSpacers()
    if (this.infoCurrent) this.setInfoCurrent()
  }

  private setTransform(val: number) {
    if (this.slides.length > this.getCurrentSlidesQty()) {
      this.inner.style.transform = this.isRTL ? `translate(${val}px, 0px)` : `translate(${-val}px, 0px)`
    } else this.inner.style.transform = 'translate(0px, 0px)'
  }

  private setTranslate(val: number) {
    this.inner.style.transform = this.isRTL ? `translate(${-val}px, 0px)` : `translate(${val}px, 0px)`
  }

  private setIndex(i: number) {
    this.currentIndex = i

    this.addCurrentClass()
    if (!this.isInfiniteLoop) this.addDisabledClass()
  }

  // Public methods
  public recalculateWidth() {
    this.sliderWidth = this.inner.parentElement.getBoundingClientRect().width

    this.calculateWidth()

    if (this.sliderWidth !== this.inner.parentElement.getBoundingClientRect().width) {
      this.recalculateWidth()
    }
  }

  public goToPrev() {
    if (this.currentIndex > 0) {
      this.currentIndex--
    } else {
      this.currentIndex = this.slides.length - this.getCurrentSlidesQty()
    }

    this.fireEvent('update', this.currentIndex)

    if (this.isSnap) {
      const itemWidth = this.sliderWidth / this.getCurrentSlidesQty()

      this.container.scrollBy({
        left: Math.max(-this.container.scrollLeft, -itemWidth),
        behavior: 'smooth'
      })

      this.addCurrentClass()
      if (!this.isInfiniteLoop) this.addDisabledClass()
    } else this.calculateTransform()

    if (this.dots) this.setCurrentDot()
  }

  public goToNext() {
    const statement = this.isCentered
      ? this.slides.length - this.getCurrentSlidesQty() + (this.getCurrentSlidesQty() - 1)
      : this.slides.length - this.getCurrentSlidesQty()

    if (this.currentIndex < statement) {
      this.currentIndex++
    } else {
      this.currentIndex = 0
    }

    this.fireEvent('update', this.currentIndex)

    if (this.isSnap) {
      const itemWidth = this.sliderWidth / this.getCurrentSlidesQty()
      const maxScrollLeft = this.container.scrollWidth - this.container.clientWidth

      this.container.scrollBy({
        left: Math.min(itemWidth, maxScrollLeft - this.container.scrollLeft),
        behavior: 'smooth'
      })

      this.addCurrentClass()
      if (!this.isInfiniteLoop) this.addDisabledClass()
    } else this.calculateTransform()

    if (this.dots) this.setCurrentDot()
  }

  public goTo(i: number) {
    const currentIndex = this.currentIndex
    this.currentIndex = i

    this.fireEvent('update', this.currentIndex)

    if (this.isSnap) {
      const itemWidth = this.sliderWidth / this.getCurrentSlidesQty()
      const index =
        currentIndex > this.currentIndex ? currentIndex - this.currentIndex : this.currentIndex - currentIndex
      const width = currentIndex > this.currentIndex ? -(itemWidth * index) : itemWidth * index

      this.container.scrollBy({
        left: width,
        behavior: 'smooth'
      })

      this.addCurrentClass()
      if (!this.isInfiniteLoop) this.addDisabledClass()
    } else this.calculateTransform()

    if (this.dots) this.setCurrentDot()
  }

  public destroy() {
    // Remove classes
    if (this.loadingClassesAdd) {
      if (typeof this.loadingClassesAdd === 'string') {
        this.inner.classList.remove(this.loadingClassesAdd)
      } else this.inner.classList.remove(...this.loadingClassesAdd)
    }
    if (this.inner && this.afterLoadingClassesAdd) {
      setTimeout(() => {
        if (typeof this.afterLoadingClassesAdd === 'string') {
          this.inner.classList.remove(this.afterLoadingClassesAdd)
        } else this.inner.classList.remove(...this.afterLoadingClassesAdd)
      })
    }
    this.el.classList.remove('init')
    this.inner.classList.remove('dragging')
    this.slides.forEach(el => el.classList.remove('active'))
    if (this?.dotsItems?.length) {
      this.dotsItems.forEach(el => el.classList.remove('active'))
    }
    this.prev.classList.remove('disabled')
    this.next.classList.remove('disabled')

    // Remove styles
    this.inner.style.width = ''
    this.slides.forEach(el => (el.style.width = ''))
    if (!this.isSnap) this.inner.style.transform = ''
    if (this.isAutoHeight) this.inner.style.height = ''

    // Remove listeners
    this.prev.removeEventListener('click', this.onPrevClickListener)
    this.next.removeEventListener('click', this.onNextClickListener)
    this.container.removeEventListener('scroll', this.onContainerScrollListener)
    this.el.removeEventListener('touchstart', this.onElementTouchStartListener)
    this.el.removeEventListener('touchend', this.onElementTouchEndListener)
    this.inner.removeEventListener('mousedown', this.onInnerMouseDownListener)
    this.inner.removeEventListener('touchstart', this.onInnerTouchStartListener)
    document.removeEventListener('mousemove', this.onDocumentMouseMoveListener)
    document.removeEventListener('touchmove', this.onDocumentTouchMoveListener)
    document.removeEventListener('mouseup', this.onDocumentMouseUpListener)
    document.removeEventListener('touchend', this.onDocumentTouchEndListener)
    this.inner.querySelectorAll('a:not(.prevented-click)').forEach(el => {
      el.classList.remove('prevented-click')
      el.removeEventListener('click', this.removeClickEventWhileDragging)
    })
    if (this?.dotsItems?.length || this.dots.querySelectorAll(':scope > *').length) {
      const dots = this?.dotsItems || this.dots.querySelectorAll(':scope > *')

      dots.forEach(el => el.removeEventListener('click', this.onDotClickListener))

      this.dots.innerHTML = null
    }

    // Remove elements
    this.inner.querySelector('.snap-before').remove()
    this.inner.querySelector('.snap-after').remove()

    this.dotsItems = null

    this.isDragging = false
    this.dragStartX = null
    this.initialTranslateX = null

    window.$hsCarouselCollection = window.$hsCarouselCollection.filter(({ element }) => element.el !== this.el)
  }

  // Static methods
  static getInstance(target: HTMLElement | string, isInstance?: boolean) {
    const elInCollection = window.$hsCarouselCollection.find(
      el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target)
    )

    return elInCollection ? (isInstance ? elInCollection : elInCollection.element) : null
  }

  static autoInit() {
    if (!window.$hsCarouselCollection) window.$hsCarouselCollection = []

    if (window.$hsCarouselCollection) {
      window.$hsCarouselCollection = window.$hsCarouselCollection.filter(({ element }) => document.contains(element.el))
    }

    document.querySelectorAll('[data-carousel]:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => {
      if (!window.$hsCarouselCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) {
        new HSCarousel(el)
      }
    })
  }
}

declare global {
  interface Window {
    HSCarousel: Function
    $hsCarouselCollection: ICollectionItem<HSCarousel>[]
  }
}

window.addEventListener('load', () => {
  HSCarousel.autoInit()

  // Uncomment for debug
  // console.log('Carousel collection:', window.$hsCarouselCollection);
})

if (typeof window !== 'undefined') {
  window.HSCarousel = HSCarousel
}

export default HSCarousel
